Events Provider
Provider that supplies data about events for the selected trading instrument - corporate actions, corporate calendar, earning reports.
/*** Interface for receiving events data** Events: dividends, splits, earnings, conference calls** After loading dxCharts library chart, events data is taken from this interface** When connecting dxCharts library, developer can implement this interface or use the default implementation [com.devexperts.dxcharts.provider.events.DxFeedEventsProvider] and pass it to the library using [DxChartsDataProviders] data class *** Use [dataFlow] to get events data** Use [changeSymbol] to change events' instrument symbol*/interface DxChartsEventsProvider {/*** Flow of receiving events data** Events data is represented by [Events] - [Events.dividends], [Events.splits], [Events.earnings], [Events.conferenceCalls]*/val dataFlow: StateFlow<Events>/*** Changes events' instrument symbol** @param symbol instrument symbol*/fun changeSymbol(symbol: String)}
Method: changeSymbol
This method is used to change the symbol of the instrument for which the events data is provided. It takes the following parameter:
symbol
: The symbol of the selected instrument. A symbol is a unique identifier used for trading on the exchange. For example, the symbol for Apple stocks on NASDAQ is "AAPL".
Example
Here is an example of how to use the changeSymbol
method:
eventsProvider.changeSymbol("AAPL")
Data is sent by updating the state of the dataFlow
variable. Class com.devexperts.dxcharts.provider.domain.Events
- an object storing arrays of all events on the chart:
/*** Data class for storing data about events (dividends, earnings, splits, conference calls)** @property dividends List of [DividendsEvent] objects representing the dividends events.* @property earnings List of [EarningsEvent] objects representing the earnings events.* @property splits List of [SplitsEvent] objects representing the splits events.* @property conferenceCalls List of [ConferenceCallsEvent] objects representing the conference calls events.*/data class Events(val dividends: List<DividendsEvent>,val earnings: List<EarningsEvent>,val splits: List<SplitsEvent>,val conferenceCalls: List<ConferenceCallsEvent>) {/*** Data class for storing data about a dividends event.** @property gross Gross value of the dividends.* @property timestamp Timestamp of the dividends event in milliseconds.*/data class DividendsEvent(val gross: Float,val timestamp: Long)/*** Data class for storing data about an earnings event.** @property basic Basic value of earnings (e.g., 4.19).* @property diluted Diluted value of earnings (e.g., 4.16).* @property timestamp Timestamp of the earnings event in milliseconds.* @property periodEnding Timestamp in milliseconds of the period ending (e.g., Sep 2023).*/data class EarningsEvent(val basic: Float,val diluted: Float,val timestamp: Long,val periodEnding: Long)/*** Data class for storing data about a splits event.** @property splitFrom "Split from" value.* @property splitTo "Split to" value.* @property timestamp Timestamp of the splits event in milliseconds.*/data class SplitsEvent(val splitFrom: Float,val splitTo: Float,val timestamp: Long)/*** Data class for storing data about a conference calls event.** @property timestamp Timestamp of the conference calls event in milliseconds.* @property referencePeriod Reference period of the conference calls event. This is a string that usually represents a quarter and year (e.g., "Q3 2021"), but the format may vary depending on the data source.*/data class ConferenceCallsEvent(val timestamp: Long,val referencePeriod: String,)}
Events set on the chart look as follows:
Here is the default implementation of DxChartsEventsProvider
:
/*** Default implementation of [DxChartsEventsProvider]** Uses dxFeed API to get events data** Uses the DxFeed api via Http requests using [OkHttpClient]** Path to get dividends data: corporate-action/symbol/ [CORPORATE_ACTIONS]** Path to get events data: corporate-action/symbol/ [CORPORATE_ACTIONS]** Path to get conference calls data: corporate-calendar/symbol/ [EARNING]** Path to get earnings data: earning-report/symbol/ [EARNING]** A token [token] is used for authorization with Base64 encoding** When loading dxCharts library and starting the provider ([changeSymbol]), a request is sent with the current symbol of the instrument ([symbol])* with 2 paths - [CORPORATE_ACTIONS], [EARNING] using [requestCorporateActions] and [requestEarnings] methods** Query responses results are put into objects [EventsResponse].** Then the data is parsed and divided into objects [DxFeedCorporateAction] and [DxFeedEarning].** The next step is the conversion and normalization of data into data classes used by the library [Events.SplitsEvent], [Events.DividendsEvent], [Events.EarningsEvent], [Events.ConferenceCallsEvent] with extensions [convertAndNormalizeSplits], [convertAndNormalizeDividends], [convertAndNormalizeEarnings], [convertAndNormalizeCalls]** After this, the data is transferred to [Events] object and sent to [dataFlow]** Requests occurs once every 10 seconds ([UPDATE_DELAY]).* If an error occurs during the request, it is repeated every 500 milliseconds ([REQUEST_DELAY])** The data retrieval logic is executed in a separate thread using [ExecutorService] [executorService].** Inside a separate thread [requester] method id executed - method that executes queries and processes responses.** Changing the symbol of the instrument and starting the work of the provider occurs using [changeSymbol] method - if the provider has already been started, then the thread with it stops, a new thread with a new symbol is started** To manually stop the provider, you can use [disconnect] method. It stops the provider, cleans [executorService] and disconnects from dxFeed API.** @property dataFlow flow of received and processed events data* @property gson Gson object for parsing json* @property executorService executor service with for running requests in separate thread (creating with Executors.newFixedThreadPool(1))* @property _dataFlow internal [MutableStateFlow] of received and processed events data* @property dataFlow public [StateFlow] of received and processed events data that is getting state from [_dataFlow]* @property client [OkHttpClient] for sending requests* @property request [Request] for sending requests with "Authorization" header with [token]* @property loaded flag that shows if data was successfully loaded in [requester] method* @property connected flag that shows if provider is working and connected to dxFeed API* @property symbol current symbol of selected instrument* @property _errorFlow Internal [MutableStateFlow] for sending errors.* @property errorFlow [StateFlow] for sending errors.*/class DxFeedEventsProvider(private val url: String = "",private val token: String = "",) : DxChartsEventsProvider, DxChartsErrorProvider<EventsProviderError> {override val dataFlow: StateFlow<Events>get() = _dataFlowprivate val gson = Gson()private var executorService: ExecutorService? = nullprivate val _dataFlow = MutableStateFlow(Events.EMPTY)private val _errorFlow: MutableStateFlow<EventsProviderError?> = MutableStateFlow(null)override val errorFlow: StateFlow<EventsProviderError?> get() = _errorFlowprivate val client = OkHttpClient().newBuilder().build()private val request = Request.Builder().addHeader("Authorization", token)@Volatileprivate var loaded = false@Volatileprivate var connected = false@Volatileprivate var symbol: String? = null/*** Changes events' instrument symbol and starts/restarts provider* If provider is already running, then it will be stopped and new thread with new symbol will be started** [requester] method will be called with new symbol in new thread** Initial events data after starting/restarting provider will be empty [Events.EMPTY]** [connected] flag will be set to true** @param symbol instrument symbol*/override fun changeSymbol(symbol: String) {executorService?.let {if (!it.isShutdown) {try {it.shutdownNow()} catch (e: Exception) {_errorFlow.tryEmit(EventsProviderError.UnknownError("Failed to shutdown executor service: $e.message", e))}}}this.symbol = symbolconnected = true_dataFlow.tryEmit(Events.EMPTY)executorService = Executors.newFixedThreadPool(1)executorService?.execute {requester()}}/*** Stops provider, cleans [executorService] and disconnects from dxFeed API** [connected] flag will be set to false** [loaded] flag will be set to true** [executorService] will be shutdown*/fun disconnect() {connected = falseloaded = truetry {executorService?.shutdownNow()} catch (e: Exception) {_errorFlow.tryEmit(EventsProviderError.UnknownError("Failed to shutdown executor service: $e.message", e))}}/*** Method for a looped requesting and processing ([requestEvents]) events data from dxFeed API with period [UPDATE_DELAY]** Loop will be stopped if [connected] flag will be set to false** Method also contains a second loop that will repeat requests at [REQUEST_DELAY] intervals if an error occurs during the request*/private fun requester() {try {_errorFlow.tryEmit(null)while (connected) {loaded = falsewhile (!loaded) {requestEvents()try {Thread.sleep(REQUEST_DELAY)} catch (e: InterruptedException) {_errorFlow.tryEmit(EventsProviderError.InterruptionError("Requester loop interrupted by thread interruption"))}}try {Thread.sleep(UPDATE_DELAY)} catch (e: InterruptedException) {_errorFlow.tryEmit(EventsProviderError.InterruptionError("Requester loop interrupted by thread interruption"))}}executorService?.shutdown()} catch (e: Exception) {_errorFlow.tryEmit(EventsProviderError.UnknownError("Requester loop interrupted: $e.message", e))Log.e(TAG, "Requester loop interrupted", e)}}/*** Method for requesting and processing events data from dxFeed API** Method sends a request with the current symbol of the instrument ([symbol]) ([symbol])* with 3 paths - [CORPORATE_ACTIONS], [EARNING] using [requestCorporateActions] and [requestEarnings] methods.** Query responses results are put into objects [EventsResponse]** Then the data is parsed and divided into objects [DxFeedCorporateAction] and [DxFeedEarning]** The next step is the conversion and normalization of data into data classes used by the library [Events.SplitsEvent], [Events.DividendsEvent], [Events.EarningsEvent], [Events.ConferenceCallsEvent] with extensions [convertAndNormalizeSplits], [convertAndNormalizeDividends], [convertAndNormalizeEarnings], [convertAndNormalizeCalls] with extensions [convertAndNormalizeSplits], [convertAndNormalizeDividends], [convertAndNormalizeEarnings], [convertAndNormalizeCalls]** After this, the data is transferred to [Events] object and sent to [dataFlow]*/private fun requestEvents() {val corporateActionsResponse = requestCorporateActions()val earningsAndCallsResponse = requestEarnings()val earningsAndCalls = if (earningsAndCallsResponse.status == OK) {parseListData<DxFeedEarning>(earningsAndCallsResponse.data ?: "[]")} else emptyList()val dividendsAndSplits = if (corporateActionsResponse.status == OK) {parseListData<DxFeedCorporateAction>(corporateActionsResponse.data ?: "[]")} else emptyList()val earnings = earningsAndCalls.filter { it.isEarning() }val conferenceCalls = earningsAndCalls.filter { it.isConferenceCall() }val dividends = dividendsAndSplits.filter { it.isDividend() }val splits = dividendsAndSplits.filter { it.isStockSplit() }_dataFlow.tryEmit(Events(dividends = dividends.convertAndNormalizeDividends(),splits = splits.convertAndNormalizeSplits(),conferenceCalls = conferenceCalls.convertAndNormalizeCalls(),earnings = earnings.convertAndNormalizeEarnings()).apply {Log.d(TAG, "events loaded")})if (earningsAndCallsResponse.status == OK || corporateActionsResponse.status == OK) {loaded = true}}/*** Method for requesting corporate actions data from dxFeed API** Sends a request for dividends and splits data with the current instrument symbol ([symbol]) to the path [URL] + [CORPORATE_ACTIONS]** The request is made using [OkHttpClient] [client] and [Request] [request]** @return [EventsResponse] with status [EventsResponse.Status.OK] and data if request was successful or with status [EventsResponse.Status.FAILED] and message if request was failed*/private fun requestCorporateActions(): EventsResponse<String> {val corpActionsRequest = request.url(url + CORPORATE_ACTIONS + symbol).build()val corpActionsCall = client.newCall(corpActionsRequest)return try {corpActionsCall.execute().use { response ->if (response.isSuccessful) {val body = response.body?.string()?: return EventsResponse(status = EventsResponse.Status.EMPTY)Log.d(TAG, "corporate actions loaded")EventsResponse(status = OK, data = body)} else {_errorFlow.tryEmit(EventsProviderError.NetworkError("Corporate actions request failed: $response.message $response.code"))EventsResponse(status = EventsResponse.Status.FAILED,message = "$response.message $response.code")}}} catch (e: Exception) {_errorFlow.tryEmit(EventsProviderError.NetworkError("Corporate actions request exception: $e.message"))EventsResponse(status = EventsResponse.Status.FAILED,message = e.message)}}/*** Method for requesting earnings data from dxFeed API** Sends a request for conference calls data with the current instrument symbol ([symbol]) to the path [url] + [EARNING]** The request is made using [OkHttpClient] [client] and [Request] [request]** @return [EventsResponse] with status [EventsResponse.Status.OK] and data if request was successful or with status [EventsResponse.Status.FAILED] and message if request was failed*/private fun requestEarnings(): EventsResponse<String> {val earningRequest = request.url(url + EARNING + symbol).build()val earningCall = client.newCall(earningRequest)return try {earningCall.execute().use { response ->if (response.isSuccessful) {val body = response.body?.string()?: return EventsResponse(status = EventsResponse.Status.EMPTY)Log.d(TAG, "earnings loaded")return EventsResponse(status = OK, data = body)} else {_errorFlow.tryEmit(EventsProviderError.NetworkError("Earning reports request failed: $response.message $response.code"))EventsResponse(status = EventsResponse.Status.FAILED,message = "$response.message $response.code")}}} catch (e: Exception) {_errorFlow.tryEmit(EventsProviderError.NetworkError("Earning reports request exception: $e.message"))EventsResponse(status = EventsResponse.Status.FAILED,message = e.message)}}/*** Internal data class for storing response from dxFeed API** @param status status of the response* @param data data of the response* @param message message of the response*/private data class EventsResponse<T>(val status: Status,val data: T? = null,val message: String? = null) {/*** Status of the response*/enum class Status {OK, FAILED, EMPTY}}/*** Extension function for parsing json string to list of objects of type [T]** @param str json string* @return list of objects*/@Suppress("UNCHECKED_CAST")private inline fun <reified T> parseListData(str: String): List<T> {val type = TypeToken.getParameterized(List::class.java, T::class.java)return try {gson.fromJson(str, type) as List<T>? ?: emptyList()} catch (e: Exception) {_errorFlow.tryEmit(EventsProviderError.ParsingError("Parsing error: $e.message", e))emptyList()}}/*** Extension function for converting and normalizing list of [DxFeedEarning] to list of [Events.EarningsEvent]*/private fun List<DxFeedEarning>.convertAndNormalizeEarnings(): List<Events.EarningsEvent> {return filter { !it.eps.isNaN() && !it.estimatedEPS.isNaN() }.map {Events.EarningsEvent(basic = it.eps,diluted = it.estimatedEPS,periodEnding = it.ymd.parseYmd(),timestamp = it.ymd.parseYmd())}.distinctBy { it.timestamp }}/*** Extension function for converting and normalizing list of [DxFeedEarning] to list of [Events.ConferenceCallsEvent]** @return list of [Events.ConferenceCallsEvent]*/private fun List<DxFeedEarning>.convertAndNormalizeCalls(): List<Events.ConferenceCallsEvent> {return map {Events.ConferenceCallsEvent(timestamp = it.ymd.parseYmd(),referencePeriod = it.referencePeriod ?: "",)}.distinctBy { it.timestamp }}/*** Extension function for converting and normalizing list of [DxFeedCorporateAction] to list of [Events.SplitsEvent]** @return list of [Events.SplitsEvent]*/private fun List<DxFeedCorporateAction>.convertAndNormalizeSplits(): List<Events.SplitsEvent> {return map {Events.SplitsEvent(splitFrom = it.ext?.splitFrom ?: 0f,splitTo = it.ext?.splitTo ?: 0f,timestamp = it.exYmd.parseYmd())}.distinctBy { it.timestamp }}/*** Extension function for converting and normalizing list of [DxFeedCorporateAction] to list of [Events.DividendsEvent]** @return list of [Events.DividendsEvent]*/private fun List<DxFeedCorporateAction>.convertAndNormalizeDividends(): List<Events.DividendsEvent> {return map {Events.DividendsEvent(gross = it.adjustmentValue ?: 0f,timestamp = it.exYmd.parseYmd())}}/*** Extension function for converting [Int] in format YYYYMMDD to [Long] timestamp in milliseconds** @return timestamp in milliseconds*/private fun Int.parseYmd(): Long {val year = this / 10000val month = (this - year * 10000) / 100val day = this - year * 10000 - month * 100return Calendar.getInstance(TimeZone.getTimeZone("UTC")).apply {set(Calendar.YEAR, year)set(Calendar.MONTH, month - 1)set(Calendar.DAY_OF_MONTH, day)set(Calendar.HOUR_OF_DAY, 0)set(Calendar.MINUTE, 0)set(Calendar.SECOND, 0)set(Calendar.MILLISECOND, 0)}.timeInMillis}companion object {const val TAG = "DxFeedEventsProvider"const val REQUEST_DELAY = 2000Lprivate const val UPDATE_DELAY = 10000Lprivate const val CORPORATE_ACTIONS = "corporate-action/symbol/"private const val EARNING = "earning/symbol/"}}/**- Sealed class for representing different types of errors in the DxFeedEventsProvider.*/sealed class EventsProviderError(override val message: String, override val error: Throwable?) :ProviderError {data class NetworkError(override val message: String,override val error: Throwable? = null) : EventsProviderError(message, error)data class InterruptionError(override val message: String,override val error: Throwable? = null) : EventsProviderError(message, error)data class ParsingError(override val message: String,override val error: Throwable? = null) : EventsProviderError(message, error)data class UnknownError(override val message: String,override val error: Throwable? = null) : EventsProviderError(message, error)}